﻿using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DistributedLocking;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
using Volo.Abp.Timing;
using Volo.Abp.Uow;
using Xms.Abp.Threading;

namespace Xms.Abp.BackgroundJob;

public class BackgroundScheduleManageTrigger: IBackgroundScheduleManageTrigger
{
    protected const string DistributedLockName = "BackgroundScheduleManageTrigger";

    protected XmsBackgroundScheduleTriggerOptions ScheduleOptions { get; }

    protected IAbpDistributedLock DistributedLock { get; }

    protected ICurrentTenant CurrentTenant { get; }

    protected IServiceScopeFactory ServiceScopeFactory { get; }

    protected AbpAsyncTimer Timer { get; }

    public IAbpLazyServiceProvider LazyServiceProvider { get; set; }

    protected ILoggerFactory LoggerFactory => LazyServiceProvider.LazyGetRequiredService<ILoggerFactory>();

    protected ILogger Logger => LazyServiceProvider.LazyGetService<ILogger>(provider => LoggerFactory?.CreateLogger(GetType().FullName) ?? NullLogger.Instance);

    protected IClock Clock => LazyServiceProvider.LazyGetRequiredService<IClock>();

    protected CancellationTokenSource StoppingTokenSource { get; private set; }
    protected CancellationToken StoppingToken { get; private set; }
    protected DateTime LastExecuteTime { get; set; }
    private Dictionary<string, IBackgroundSchedule> _backgroundSchedules;
    private bool IsRunning { get; set; }

    public BackgroundScheduleManageTrigger(
        AbpAsyncTimer timer,
        IOptions<XmsBackgroundScheduleTriggerOptions> scheduleOptions,
        IServiceScopeFactory serviceScopeFactory,
        IAbpLazyServiceProvider lazyServiceProvider,
        IAbpDistributedLock distributedLock,
        ICurrentTenant currentTenant)
    {
        ServiceScopeFactory = serviceScopeFactory;
        LazyServiceProvider = lazyServiceProvider;
        Timer = timer;
        Timer.Elapsed = TimerElapsed;
        DistributedLock = distributedLock;
        ScheduleOptions = scheduleOptions.Value;
        Timer.Period = ScheduleOptions.TriggerPollPeriod;
        CurrentTenant = currentTenant;
        _backgroundSchedules = new Dictionary<string, IBackgroundSchedule>();

        foreach (var scheduleType in ScheduleOptions.ScheduleTypes)
        {
            var schedule = lazyServiceProvider.LazyGetRequiredService(scheduleType) as IBackgroundSchedule;
            var attr = schedule.GetType().GetCustomAttribute<BackgroundScheduleNameAttribute>();
            _backgroundSchedules[attr?.Name ?? schedule.ToString()] = schedule;
        }
    }

    public virtual async Task StartAsync(CancellationToken cancellationToken = default)
    {
        if (IsRunning) return;
        IsRunning = true;
        StoppingTokenSource ??= new CancellationTokenSource();
        StoppingToken = StoppingTokenSource.Token;
        using (var scope = ServiceScopeFactory.CreateScope())
        {
            var serviceProvider = scope.ServiceProvider;
            var uowm = serviceProvider.GetRequiredService<IUnitOfWorkManager>();
            using (var uow = uowm.Begin(true, false))
            {
                try
                {
                    await InitializationSchedulesAsync(serviceProvider, cancellationToken);
                    //await uow.SaveChangesAsync();
                    await uow.CompleteAsync();
                }
                catch (Exception)
                {
                    await uow.RollbackAsync();
                    throw;
                }
            }
        }

        Logger.LogDebug("Started background schedule manage trigger: " + ToString());
        await Task.CompletedTask;
        Timer.Start(cancellationToken);
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken = default)
    {
        if (!IsRunning) return;
        IsRunning = false;
        Timer.Stop(cancellationToken);
        using (var scope = ServiceScopeFactory.CreateScope())
        {
            var serviceProvider = scope.ServiceProvider;
            foreach (var schedule in _backgroundSchedules.Values)
            {
                await schedule.StopAsync(cancellationToken);
            }
        }
        Logger.LogDebug("Stopped background schedule manage trigger: " + ToString());
        StoppingTokenSource?.Cancel();
        StoppingTokenSource?.Dispose();
        StoppingTokenSource = null;
        await Task.CompletedTask;
    }

    public override string ToString()
    {
        return GetType().FullName;
    }

    private async Task TimerElapsed(AbpAsyncTimer timer)
    {
        using (var scope = ServiceScopeFactory.CreateScope())
        {
            var serviceProvider = scope.ServiceProvider;
            var uowm = serviceProvider.GetRequiredService<IUnitOfWorkManager>();
            using (var uow = uowm.Begin(true, false))
            {
                try
                {
                    await using var handler = await DistributedLock.TryAcquireAsync(DistributedLockName, cancellationToken: StoppingToken);
                    if (handler != null)
                    {
                        await ManageSchedulesAsync(serviceProvider);
                    }
                    else
                    {
                        try
                        {
                            await Task.Delay(ScheduleOptions.TriggerPollPeriod * 12, StoppingToken);
                        }
                        catch (TaskCanceledException) { }
                    }
                    //await uow.SaveChangesAsync();
                    await uow.CompleteAsync();
                }
                catch (Exception ex)
                {
                    await uow.RollbackAsync();
                    await scope.ServiceProvider
                        .GetRequiredService<IExceptionNotifier>()
                        .NotifyAsync(new ExceptionNotificationContext(ex));

                    Logger.LogException(ex);
                }
            }
        }
    }
    private Dictionary<string, BackgroundScheduleInfo> Context { get; set; }
    private DateTime LastCheckTime { get; set; }
    protected async Task InitializationSchedulesAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken = default)
    {
        var dict = _backgroundSchedules.ToDictionary(r => r.Key, r =>
        {
            var attr = r.Value.GetType().GetCustomAttribute<BackgroundScheduleNameAttribute>();
            var nextExecuteTime = Cron.Parse(r.Value.CronExpression).GetNextOccurrence(DateTime.Now) ?? DateTime.MaxValue;
            var v = new BackgroundScheduleInfo
            {
                Name = attr?.Name ?? r.Value.ToString(),
                DisplayName = attr?.DisplayName ?? r.Value.ToString(),

                CreationTime = DateTime.Now,
                Cron = r.Value.CronExpression,
                ExecuteCount = 0,
                IsEnabled = true,
                LastExecuteTime = Clock.Now,
                ModificationTime = DateTime.Now,
                MultiTenancySides = r.Value.MultiTenancySides,
                NextExecuteTime = nextExecuteTime,
            };
            return v;
        });
        var store = serviceProvider.GetRequiredService<IBackgroundScheduleStore>();

        var data = (await store.GetAllSchedulesAsync(cancellationToken)).ToDictionary(r => r.Name);
        LastCheckTime = Clock.Now;
        foreach (var sn in dict.Keys)
        {
            if (!data.ContainsKey(sn))
            {
                await store.InsertAsync(dict[sn], cancellationToken);
                data[sn] = dict[sn];
            }
            else
            {
                dict[sn].Id = data[sn].Id;
            }
        }
        foreach (var sn in data.Keys)
        {
            if (sn.StartsWith("#Dynamic#"))
            {
                dict.Add(sn, data[sn]);
                _backgroundSchedules.Add(sn, LazyServiceProvider.LazyGetRequiredService<DynamicBackgroundSchedule>());
            }
            else if (!dict.ContainsKey(sn))
            {
                await store.DeleteAsync(sn, cancellationToken);
                data.Remove(sn);
            }
        }

        Context = data;

        foreach (var scheduleName in _backgroundSchedules.Keys)
        {
            var schedule = _backgroundSchedules[scheduleName];
            schedule.BackgroundScheduleInfo = data[scheduleName];
            var v = schedule.BackgroundScheduleInfo;
            if (v.NextExecuteTime < DateTime.Now)
            {
                v.NextExecuteTime = Cron.Parse(v.Cron).GetNextOccurrence(DateTime.Now) ?? DateTime.MaxValue;
                await store.UpdateAsync(v, cancellationToken);
            }
            schedule.MultiTenancySides = v.MultiTenancySides;
            schedule.CronExpression = v.Cron;
            if (v.IsEnabled)
            {
                await schedule.StartAsync(cancellationToken);
            }
            if (schedule is BackgroundSchedule bs)
            {
                bs.OnExecuted = async (t, s, n, c) =>
                {
                    var sn = _backgroundSchedules.First(r => r.Value == t).Key;
                    var val = data[sn];
                    val.LastExecuteTime = Clock.Now;
                    val.NextExecuteTime = n ?? DateTime.MaxValue;
                    val.ExecuteCount++;
                    var store = s.GetRequiredService<IBackgroundScheduleStore>();
                    await store.UpdateAsync(val, c);
                };
            }
        }
    }

    protected async Task ManageSchedulesAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken = default)
    {
        var dict = Context;
        var store = serviceProvider.GetRequiredService<IBackgroundScheduleStore>();

        var list = await store.GetAllSchedulesAsync(cancellationToken);
        var data = list.Where(r => r.ModificationTime >= LastCheckTime).ToDictionary(r => r.Name);
        LastCheckTime = Clock.Now;

        foreach (var sn in data.Keys)
        {
            var v = data[sn];
            if (!dict.ContainsKey(sn))
            {
                if (sn.StartsWith("#Dynamic#"))
                {
                    dict.Add(sn, data[sn]);
                    _backgroundSchedules.Add(sn, LazyServiceProvider.LazyGetRequiredService<DynamicBackgroundSchedule>());
                }
                else
                {
                    continue;
                }
            }
            var s = dict[sn];
            var schedule = _backgroundSchedules[sn];
            if (s.Cron != v.Cron)
            {
                schedule.CronExpression = v.Cron;
                s.Cron = v.Cron;
                if (schedule is BackgroundSchedule bs && !bs.IsRunning)
                {
                    await schedule.StartAsync();
                }
            }
            if (s.MultiTenancySides != v.MultiTenancySides)
            {
                schedule.MultiTenancySides = v.MultiTenancySides;
                s.MultiTenancySides = v.MultiTenancySides;
            }
            if (s.IsEnabled != v.IsEnabled)
            {
                s.IsEnabled = v.IsEnabled;
                if (v.IsEnabled)
                {
                    await schedule.StartAsync(cancellationToken);
                }
                else
                {
                    await schedule.StopAsync(cancellationToken);
                }
            }
            dict[sn] = v;
        }
        var allData = list.ToDictionary(r => r.Name);

        foreach (var sn in dict.Keys.ToArray())
        {
            if (!allData.ContainsKey(sn))
            {
                var schedule = _backgroundSchedules[sn];
                await schedule.StopAsync(cancellationToken);
                _backgroundSchedules.Remove(sn);
                dict.Remove(sn);
            }
        }
    }
}
